package io.demo.message.benchmark;

import static java.util.concurrent.TimeUnit.MICROSECONDS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static org.apache.commons.lang3.ClassUtils.getShortCanonicalName;
import static org.apache.commons.lang3.builder.ToStringStyle.SHORT_PREFIX_STYLE;

import java.io.Serializable;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Aspect
public class ProfilingAspect {

  private static final Logger LOGGER = LoggerFactory.getLogger(ProfilingAspect.class);


  private final ThreadLocal<Long> timestamp = ThreadLocal.withInitial(() -> 0L);

  @Pointcut("execution(public !static void io.demo.message.benchmark.Benchmark.execute(java.io.Serializable, int))")
  void execute() {
    // pointcut
  }


  @Before("execute() && args(o, looping) && target(benchmark)")
  public void markStartTime(Serializable o, int looping, Benchmark benchmark) {
    timestamp.set(System.nanoTime());
  }

  @After("execute() && args(o, looping) && target(benchmark)")
  public void calcElapsedTime(Serializable o, int looping, Benchmark benchmark) {

    // Mark the time, the very first thing to do.
    final long duration = System.nanoTime() - timestamp.get();
    final BigDecimal avgDuration = BigDecimal.valueOf(duration)
        .divide(BigDecimal.valueOf(looping), 0, RoundingMode.CEILING);
    final String benchmarkName = getShortCanonicalName(benchmark.getClass());
    final long durationMillis = MILLISECONDS.convert(duration, NANOSECONDS);
    final long avgDurationMicros = MICROSECONDS.convert(avgDuration.longValue(), NANOSECONDS);

    BenchmarkTuple benchmarkTuple = new BenchmarkTuple(
        benchmarkName, durationMillis, avgDurationMicros, looping);

    LOGGER.info("{}", benchmarkTuple);
  }

  private static class BenchmarkTuple implements Serializable {

    private static final long serialVersionUID = -9029969116883057375L;

    private static final DecimalFormat DF = new DecimalFormat("###,###");

    private String benchmarkType;

    private String durationMills;

    private String avgDurationMicros;

    private String looping;

    BenchmarkTuple(String benchmarkType, long durationMills, long avgDurationMicros,
        int looping) {
      this.benchmarkType = benchmarkType;
      this.durationMills = DF.format(durationMills);
      this.avgDurationMicros = DF.format(avgDurationMicros);
      this.looping = DF.format(looping);
    }

    @Override
    public String toString() {
      return ToStringBuilder.reflectionToString(this, SHORT_PREFIX_STYLE);
    }

    public String getBenchmarkType() {
      return benchmarkType;
    }

    public String getDurationMills() {
      return durationMills;
    }

    public String getAvgDurationMicros() {
      return avgDurationMicros;
    }

    public String getLooping() {
      return looping;
    }
  }
}